/*
 * File     : StructureBuilder.java
 * Created  : 1 May 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.structure;

import static com.googlecode.dni.internal.DniInternal.LOGGER;
import static com.googlecode.dni.internal.DniTypes.ABSTRACT_STRUCTURE_FACTORY_TYPE;
import static com.googlecode.dni.internal.DniTypes.ABSTRACT_STRUCTURE_TYPE;
import static org.objectweb.asm.Opcodes.*;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import com.googlecode.dni.internal.DniInternal;
import com.googlecode.dni.internal.call.JniNativeMethod;
import com.googlecode.dni.internal.library.PartialLibraryBuilder;
import com.googlecode.dni.internal.string.FastCharset;
import com.googlecode.dni.internal.structure.StructureManager.Session;
import com.googlecode.dni.library.Function;
import com.googlecode.dni.library.Library;
import com.googlecode.dni.type.Freeable;
import com.googlecode.dni.type.FreeableNativeObject;
import com.googlecode.dni.type.NativeObject;
import com.googlecode.dni.type.Pointer;
import com.googlecode.dni.type.structure.IllegalStructureException;
import com.googlecode.dni.type.structure.Padding;
import com.googlecode.dni.type.structure.Structure;
import com.googlecode.dni.type.structure.StructureFactory;
import com.googlecode.dni.type.structure.StructureInfo;
import com.googlecode.dni.type.structure.StructureOptions;
import com.googlecode.dni.type.structure.StructureOptions.NoLibraryType;

/**
 * <p>
 *  Generates byte-code for implementations of structure types.
 * </p>
 *
 * @param <T>
 *            the structure interface type
 *
 * @author Matthew Wilson
 */
@SuppressWarnings( "nls" )
final class StructureBuilder< T extends Structure >
{

    /** Whether we've warned about missing class files. */
    private static boolean warnedAboutMissingClassFile;


    private final Session session;

    private final Class< T > type;

    // Note: size and padding are not used in opaque at present; in theory, an
    // opaque structure *could* have defined size and padding, allowing creation
    // from user code in Java.  However, a simple workaround is to define a
    // non-opaque type with a ByteBuffer of the correct size.  This sounds like
    // a pretty weird case, anyway.
    private int size;

    private final Padding padding;

    private final String charset;

    private final Library defaultLibrary;

    private final FastCharset fastCharset;

    /** The interface name (in byte-code style). */
    private final String interfaceName;

    /** The type implementation name (in byte-code style). */
    private final String implName;

//    private final String partialLibraryImplName;

    private final org.objectweb.asm.Type implType;

    private final String factoryImplName;

    private final Map< Library, StructureMethodsRecord > structureMethodsMap =
        new LinkedHashMap< Library, StructureMethodsRecord >();

    private final StructureSubBuilder structureSubBuilder;

    private final boolean opaque;

    private final ClassWriter classWriter;

    private final List< JniNativeMethod > nativeMethods = new ArrayList< JniNativeMethod >();

    private Class< ? > implClass;


    //-- Main API ----------------------------------------------------------

    /**
     * Creates a new instance.
     *
     * @param <T>
     *            the structure interface type
     * @param session
     *            the owning session creating this structure
     * @param type
     *            the structure interface type
     * @param implName
     *            the implementation type name, in byte-code style
     * @param factoryImplName
     *            the factory implementation type name, in byte-code style
     * @return a new instance
     */
    static < T extends Structure > StructureBuilder< T > create( final StructureManager.Session session,
                                                                 final Class< T > type,
                                                                 final String implName,
                                                                 final String factoryImplName )
    {
        if ( !type.isInterface() )
        {
            throw new IllegalStructureException( "Structure must an interface: " + type.getName() );
        }

        if ( type.getTypeParameters().length > 0 )
        {
            throw new IllegalStructureException( "Structure cannot have type parameters: " + type.getName() );
        }

        if ( !Structure.class.isAssignableFrom( type ) )
        {
            throw new IllegalStructureException( "Structure must extend Structure interface: " + type.getName() );
        }

        StructureOptions options = type.getAnnotation( StructureOptions.class );
        int size = -1;
        Padding padding = Padding.DEFAULT;
        boolean opaque = false;
        String charset = Library.DEFAULT_CHARSET;
        Library defaultLibrary = null;
        if ( options != null )
        {
            size = options.size();
            padding = options.padding();
            opaque = options.opaque();
            charset = options.charset();
            defaultLibrary = options.library();
            Class< ? > libraryType = options.libraryType();
            if ( libraryType != NoLibraryType.class )
            {
                if ( !Arrays.equals( defaultLibrary.name(), new String[] { StructureOptions.NO_LIBRARY } ) )
                {
                    throw new IllegalStructureException( "Can specify a @Library or a type that is "
                                                         + "annotated with @Library, but not both: " + type.getName() );
                }
                defaultLibrary = libraryType.getAnnotation( Library.class );
                if ( defaultLibrary == null )
                {
                    throw new IllegalStructureException( "Cannot specify a library type that is not annotated with @Library: "
                                                         + type.getName() );
                }
            }
        }

        // any other interfaces extended? (excluding pointless ones)
        List< Class< ? > > superInterfaces = new ArrayList< Class< ? > >();
        Collections.addAll( superInterfaces, type.getInterfaces() );
        superInterfaces.remove( Structure.class );
        superInterfaces.remove( FreeableNativeObject.class );
        superInterfaces.remove( Freeable.class );
        superInterfaces.remove( NativeObject.class );

        if ( !superInterfaces.isEmpty() )
        {
            throw new IllegalStructureException( "Structure cannot extend multiple interfaces: " + type.getName() );
        }

        return new StructureBuilder< T >( session,
                                          type,
                                          implName,
                                          factoryImplName,
                                          size,
                                          padding,
                                          charset,
                                          defaultLibrary,
                                          opaque );
    }

    /**
     * @param session
     *            the owning session creating this structure
     * @param type
     *            the type
     * @param implName
     *            the implementation type name, in byte-code style
     * @param factoryImplName
     *            the factory implementation type name, in byte-code style
     * @param size
     *            the size of the structure in bytes, or -1
     * @param padding
     *            the padding
     * @param charset
     *            the charset
     * @param defaultLibrary
     *            the default library type
     * @param opaque
     *            whether opaque
     */
    private StructureBuilder( final StructureManager.Session session,
                              final Class< T > type,
                              final String implName,
                              final String factoryImplName,
                              final int size,
                              final Padding padding,
                              final String charset,
                              final Library defaultLibrary,
                              final boolean opaque )
    {
        this.session = session;
        this.type = type;
        this.implName = implName;
        this.factoryImplName = factoryImplName;
        this.size = size;
        this.padding = padding;
        this.charset = charset;
        this.defaultLibrary = defaultLibrary;
        this.opaque = opaque;

        this.implType = org.objectweb.asm.Type.getType( "L" + this.implName + ";" );
        this.interfaceName = this.type.getName().replace( '.', '/' );
        this.fastCharset = FastCharset.getInstance( this.charset );

        if ( opaque )
        {
            this.structureSubBuilder = new OpaqueStructureSubBuilder( this );
        }
        else
        {
            this.structureSubBuilder = new NormalStructureSubBuilder( this );
        }

        this.classWriter = new ClassWriter( ClassWriter.COMPUTE_FRAMES );
        this.classWriter.visit( Opcodes.V1_6,
                           Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL,
                           getImplName(),
                           null,
                           this.structureSubBuilder.getStructureBaseType(),
                           new String[] { this.interfaceName } );

    }

    /**
     * Performs the main build operations.
     *
     * @return the structure record
     */
    final StructureRecord build()
    {
        // determine what (relevant) methods there are
        List< Method > methods = getFieldMethods();

        // create the class
        this.structureSubBuilder.writeMainClassMethods( this.classWriter, methods );

        writeLibraryMethods();

        StructureInfo info = new StructureInfo( this.size, this.opaque );

        return new StructureRecord( this.type,
                                    this.implName,
                                    this.factoryImplName,
                                    info,
                                    this.structureSubBuilder.getLargestFieldUnitSize() );
    }

    /**
     * Finishes off the definition, and loads the class.  This does not
     * initialise it.
     *
     * @param structureRecord
     *            the structure record
     */
    final void loadClass( final StructureRecord structureRecord )
    {
        // TODO v2.0 fix the native method name conflict
        // If there is more than one set of structure methods, there will be
        // native method conflicts (native_1, native_2, ... d'oh).
        // (Not allowed yet to have more than one set because there is no
        // inheritance of structure types permitted.)

        // the structure methods may use <clinit> things, so wait until here to
        // process <clinit>
        MethodVisitor clinitMethodVisitor = beginStaticInitialiser();

        // define any structure methods
        for ( StructureMethodsRecord record : this.structureMethodsMap.values() )
        {
            PartialLibraryBuilder builder = PartialLibraryBuilder.create( record, this.implName );
            builder.build( this.classWriter, this.nativeMethods );
            builder.writeStaticInitialiserFragment( clinitMethodVisitor );
        }

        endStaticInitialiser( clinitMethodVisitor );

        // define the class in the context of the class loader
        this.classWriter.visitEnd();
        this.implClass = DniInternal.defineClass( this.implName,
                                                  this.type.getClassLoader(),
                                                  this.classWriter.toByteArray() );

        // now set the factory, which requires the structure class to have been
        // loaded properly
        StructureFactory< T > factory = createFactory();
        structureRecord.setFactory( factory );
    }

    /**
     * Initialises the class.
     *
     * @param structureRecord
     *            the structure record
     */
    final void initClass( final StructureRecord structureRecord )
    {
        StructureFactory< ? > factory = structureRecord.getFactory();

        // force the structure class to be initialised (sorry; could be done in a better way)
        // TODO v1.1 Better way to initialise type
        try
        {
            factory.fromPointer( Pointer.fromAddress( 1 ) );
        }
        catch ( Error error )
        {
            DniInternal.LOGGER.log( Level.SEVERE, "Failed to load implementation type: " + this.implName, error );
            throw error;
        }

        // register the native methods
        for ( JniNativeMethod nativeMethod : this.nativeMethods )
        {
            nativeMethod.register( this.implClass );
        }

        // prevent class GC of the impl type (not that this should be a problem)
        // TODO v1.0 Investigate; is this nonsense?
        ( (AbstractStructureFactory< ? >) factory ).init( this.implClass, structureRecord );
    }


    //-- Internal API ----------------------------------------------------------

    /** @return the owning session */
    final StructureManager.Session getSession()
    {
        return this.session;
    }

    /** @return the type */
    final Class< T > getType()
    {
        return this.type;
    }

    /** @return the size */
    final int getSize()
    {
        return this.size;
    }

    /**
     * @param size
     *            the size to set
     */
    final void setSize( final int size )
    {
        assert size >= 0;
        this.size = size;
    }

    /** @return the padding */
    final Padding getPadding()
    {
        return this.padding;
    }

    /** @return the charset */
    final String getCharset()
    {
        return this.charset;
    }

    /** @return the fastCharset */
    final FastCharset getFastCharset()
    {
        return this.fastCharset;
    }

    /**
     * @return the interfaceName
     */
    final String getInterfaceName()
    {
        return this.interfaceName;
    }

    /** @return the implName */
    final String getImplName()
    {
        return this.implName;
    }

    /** @return the implType */
    final org.objectweb.asm.Type getImplType()
    {
        return this.implType;
    }

    /** @return the factoryImplName */
    final String getFactoryImplName()
    {
        return this.factoryImplName;
    }

    /**
     * Writes the {@link Structure#toString()} method.
     */
    final void writeToStringMethod()
    {
        MethodVisitor methodVisitor = this.classWriter.visitMethod( ACC_PUBLIC,
                                                                    "toString",
                                                                    "()Ljava/lang/String;",
                                                                    null,
                                                                    null );
        methodVisitor.visitCode();

        // new StringBuilder()
        methodVisitor.visitTypeInsn( NEW, "java/lang/StringBuilder" );
        methodVisitor.visitInsn( DUP );
        methodVisitor.visitMethodInsn( INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V" );

        // .append( "<struct-name>[0x" )
        methodVisitor.visitLdcInsn( getType().getName() + "[0x" );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "append",
                                       "(Ljava/lang/String;)Ljava/lang/StringBuilder;" );

        // .append( Long.toHexString( this.pointer ) )
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitFieldInsn( GETFIELD, getImplName(), "pointer", "J" );
        methodVisitor.visitMethodInsn( INVOKESTATIC,
                                       "java/lang/Long",
                                       "toHexString",
                                       "(J)Ljava/lang/String;" );

        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "append",
                                       "(Ljava/lang/String;)Ljava/lang/StringBuilder;" );

        // .append( "]" )
        methodVisitor.visitLdcInsn( "]" );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "append",
                                       "(Ljava/lang/String;)Ljava/lang/StringBuilder;" );

        // .toString()
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "toString",
                                       "()Ljava/lang/String;" );

        // return
        methodVisitor.visitInsn( ARETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    private List< Method > getFieldMethods()
    {
        List< Method > methods = new ArrayList< Method >();
        for ( Method method : this.type.getMethods() )
        {
            if ( method.getDeclaringClass() != Object.class
                 && method.getDeclaringClass() != Structure.class
                 && method.getDeclaringClass() != NativeObject.class
                 && method.getDeclaringClass() != FreeableNativeObject.class
                 && method.getAnnotation( Function.class ) == null )
            {
                methods.add( method );
            }
        }

        // There's no guarantee of ordering; Java 6 seems to sort them, but not
        // in all circumstances (yes, that's confusing).
        // Try to find the source class file and use that for the ordering.
        // (Of course, that relies on javac ordering them correctly :-S )

        return sortMethods( methods );
    }

    /**
     * Creates the factory.
     *
     * @return the factory
     */
    private StructureFactory< T > createFactory()
    {
        ClassWriter factoryWriter = new ClassWriter( ClassWriter.COMPUTE_FRAMES );

        factoryWriter.visit( Opcodes.V1_6,
                             Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL,
                             this.factoryImplName,
                             ABSTRACT_STRUCTURE_FACTORY_TYPE + "<L" + this.interfaceName + ";>",
                             ABSTRACT_STRUCTURE_FACTORY_TYPE,
                             null );

        factoryWriter.visitField( ACC_PUBLIC | ACC_STATIC | ACC_FINAL,
                                  "INSTANCE",
                                  "L" + this.factoryImplName + ";",
                                  null,
                                  null );

        createFactoryClassInitialiser( factoryWriter );
        createFactoryConstructor( factoryWriter );
        writeUntypedCreateMethod( factoryWriter );
        writeUntypedFromPointerMethod( factoryWriter );

        byte[] classFile = factoryWriter.toByteArray();

        Class< ? > clazz = DniInternal.defineClass( this.factoryImplName,
                                                    this.type.getClassLoader(),
                                                    classFile );
        @SuppressWarnings( "unchecked" )
        Class< ? extends StructureFactory< T > > factoryClass =
            (Class< ? extends StructureFactory< T > >) clazz;

        try
        {
            @SuppressWarnings( "unchecked" )
            StructureFactory< T > structureFactory =
                (StructureFactory< T >) factoryClass.getField( "INSTANCE" ).get( null );
            return structureFactory;
        }
        catch ( Throwable throwable )
        {
            throw new IllegalStructureException( "Bad factory", throwable );
        }
    }

    /**
     * Sorts the methods according to the original class file.  Yeah, thanks a
     * BUNCH for preserving the order.
     *
     * @param methods
     *            the unsorted methods
     * @return the sorted methods
     */
    private List< Method > sortMethods( final List< Method > methods )
    {
        String name = this.type.getName().replace( '.', '/' ) + ".class";
        InputStream stream = this.type.getClassLoader().getResourceAsStream( name );
        if ( stream == null )
        {
            warnAboutMissingClassFile();
        }
        else
        {
            MethodSortingVisitor methodSortingVisitor = new MethodSortingVisitor( getType().getDeclaredMethods() );
            try
            {
                ClassReader reader = new ClassReader( stream );
                reader.accept( methodSortingVisitor,
                               ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES );

                // sort the methods...
                List< Method > sortedMethods = methodSortingVisitor.getSortedMethods();

                // ...but strip out any structure methods
                sortedMethods.retainAll( methods );

                return sortedMethods;
            }
            catch ( IOException exception )
            {
                LOGGER.log( Level.WARNING, "Failed to read class file", exception );
                warnAboutMissingClassFile();
            }
            finally
            {
                try
                {
                    stream.close();
                }
                catch ( IOException exception )
                {
                    LOGGER.log( Level.WARNING, "Failed to close input stream", exception );
                }
            }
        }
        return methods;
    }

    /**
     * Warns about missing / broken class files.
     */
    private static void warnAboutMissingClassFile()
    {
        if ( warnedAboutMissingClassFile == false )
        {
            LOGGER.warning( "Cannot file class file to determine structure field ordering" );
            LOGGER.warning( "You will receive this warning only once" );
            warnedAboutMissingClassFile = true;
        }
    }

    /**
     * Writes the library methods.
     */
    private void writeLibraryMethods()
    {
        for ( Method method : this.type.getMethods() )
        {
            if ( method.getAnnotation( Function.class ) != null )
            {
                writeLibraryMethod( method );
            }
        }
    }

    /**
     * Writes a library method.  It simply calls a static method on this type
     * that will be written by {@link PartialLibraryBuilder}.
     *
     * @param method
     *            the method
     */
    private void writeLibraryMethod( final Method method )
    {
        MethodVisitor methodVisitor = this.classWriter.visitMethod( ACC_PUBLIC,
                                                                    method.getName(),
                                                                    org.objectweb.asm.Type.getMethodDescriptor( method ),
                                                                    null,
                                                                    null );
        methodVisitor.visitCode();

        writeLoadLibraryParameters( method, methodVisitor );

        String structureMethodName = StructureMethodsRecord.getStructureMethodName( method );
        String structureMethodDescriptor = StructureMethodsRecord.getStructureMethodDescriptor( method );

        // Special case:  If this is an opaque structure where inheritance has
        // been used, we don't have to create another native method.  It also
        // fixes the issue where the inherited method has a different library.
        String structureImplName;
        Class< ? > declaringClass = method.getDeclaringClass();
        if ( declaringClass == this.type )
        {
            // TODO v2.0 this could be extended to allow specific libraries per method
            Library library = this.defaultLibrary;

            // remember this library type for post-create actions
            StructureMethodsRecord structureMethodsRecord = this.structureMethodsMap.get( library );
            if ( structureMethodsRecord == null )
            {
                structureMethodsRecord = new StructureMethodsRecord( this.type, library );
                this.structureMethodsMap.put( library, structureMethodsRecord );
            }
            structureMethodsRecord.addMethod( method );

            structureImplName = this.implName;
        }
        else
        {
            // TODO v2.0 Decide whether to support structure inheritance
            throw new AssertionError();
//            // comes from a super-interface; guaranteed to have constructed
//            // these before getting here
//            StructureRecord ancestor = DniInternal.getStructureRecord( declaringClass.asSubclass( Structure.class ) );
//            structureImplName = ancestor.getImplName();
        }
        methodVisitor.visitMethodInsn( INVOKESTATIC,
                                       structureImplName,
                                       structureMethodName,
                                       structureMethodDescriptor );

        writeReturnFromLibraryMethod( method, methodVisitor );

        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    private void writeLoadLibraryParameters( final Method method, final MethodVisitor methodVisitor )
    {
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitFieldInsn( GETFIELD,
                                      this.implName,
                                      "pointer",
                                      "J" );

        int varIndex = 1;
        for ( Class< ? > parameterType : method.getParameterTypes() )
        {
            if ( parameterType == boolean.class
                 || parameterType == byte.class
                 || parameterType == short.class
                 || parameterType == char.class
                 || parameterType == int.class )
            {
                methodVisitor.visitVarInsn( ILOAD, varIndex++ );
            }
            else if ( parameterType == long.class )
            {
                methodVisitor.visitVarInsn( LLOAD, varIndex += 2 );
            }
            else if ( parameterType == float.class )
            {
                methodVisitor.visitVarInsn( FLOAD, varIndex++ );
            }
            else if ( parameterType == double.class )
            {
                methodVisitor.visitVarInsn( DLOAD, varIndex += 2 );
            }
            else
            {
                assert Object.class.isAssignableFrom( parameterType );
                methodVisitor.visitVarInsn( ALOAD, varIndex++ );
            }
        }
    }

    private static void writeReturnFromLibraryMethod( final Method method, final MethodVisitor methodVisitor )
    {
        Class< ? > returnType = method.getReturnType();
        if ( returnType == boolean.class
             || returnType == byte.class
             || returnType == short.class
             || returnType == char.class
             || returnType == int.class )
        {
            methodVisitor.visitInsn( IRETURN );
        }
        else if ( returnType == long.class )
        {
            methodVisitor.visitInsn( LRETURN );
        }
        else if ( returnType == float.class )
        {
            methodVisitor.visitInsn( FRETURN );
        }
        else if ( returnType == double.class )
        {
            methodVisitor.visitInsn( DRETURN );
        }
        else if ( returnType == void.class )
        {
            methodVisitor.visitInsn( RETURN );
        }
        else
        {
            assert Object.class.isAssignableFrom( returnType );
            methodVisitor.visitInsn( ARETURN );
        }
    }

    /**
     * Writes the beginning of the static initialiser.
     *
     * @return the method visitor, ready to use
     */
    private MethodVisitor beginStaticInitialiser()
    {
        MethodVisitor methodVisitor = this.classWriter.visitMethod( ACC_PUBLIC,
                                                                    "<clinit>",
                                                                    "()V",
                                                                    null,
                                                                    null );
        methodVisitor.visitCode();

        this.structureSubBuilder.writeStaticInitialiserFragment( methodVisitor );

        return methodVisitor;
    }

    /**
     * Writes the end of the static initialiser.
     *
     * @param methodVisitor
     *            the method visitor
     */
    private static void endStaticInitialiser( final MethodVisitor methodVisitor )
    {
        methodVisitor.visitInsn( RETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }


    //-- StructureFactory method generation ------------------------------------

    /**
     * Writes the static initialiser.
     *
     * @param factoryWriter
     *            the factory writer
     */
    private void createFactoryClassInitialiser( final ClassWriter factoryWriter )
    {
        MethodVisitor methodVisitor = factoryWriter.visitMethod( ACC_STATIC,
                                                                 "<clinit>",
                                                                 "()V",
                                                                 null,
                                                                 null );
        methodVisitor.visitCode();
        methodVisitor.visitTypeInsn( NEW, this.factoryImplName );
        methodVisitor.visitInsn( DUP );
        methodVisitor.visitMethodInsn( INVOKESPECIAL,
                                       this.factoryImplName,
                                       "<init>",
                                       "()V" );
        methodVisitor.visitFieldInsn( PUTSTATIC,
                                      this.factoryImplName,
                                      "INSTANCE",
                                      "L" + this.factoryImplName + ";" );
        methodVisitor.visitInsn( RETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes a constructor for the factory class.
     *
     * @param factoryWriter
     *            the class writer
     */
    private void createFactoryConstructor( final ClassWriter factoryWriter )
    {
        MethodVisitor constructor = factoryWriter.visitMethod( Opcodes.ACC_PRIVATE,
                                                               "<init>",
                                                               "()V",
                                                               null,
                                                               null );
        constructor.visitCode();
        constructor.visitVarInsn( ALOAD, 0 );
        constructor.visitLdcInsn( org.objectweb.asm.Type.getType( this.type ) );
        constructor.visitMethodInsn( INVOKESPECIAL,
                                     ABSTRACT_STRUCTURE_FACTORY_TYPE,
                                     "<init>",
                                     "(Ljava/lang/Class;)V" );
        constructor.visitInsn( RETURN );
        constructor.visitMaxs( 0, 0 );
        constructor.visitEnd();
    }

    /**
     * Writes the {@link AbstractStructureFactory#allocateUntyped()} method.
     *
     * @param factoryWriter
     *            the class writer
     */
    private void writeUntypedCreateMethod( final ClassWriter factoryWriter )
    {
        MethodVisitor methodVisitor = factoryWriter.visitMethod( Opcodes.ACC_PUBLIC,
                                                                 "allocateUntyped",
                                                                 "()L" + ABSTRACT_STRUCTURE_TYPE + ";",
                                                                 null,
                                                                 null );
        methodVisitor.visitCode();

        this.structureSubBuilder.writeAllocateUntypedFactoryMethodImpl( methodVisitor );

        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes the
     * {@link AbstractStructureFactory#fromPointerUntyped(long)} method.
     *
     * @param factoryWriter
     *            the class writer
     */
    private void writeUntypedFromPointerMethod( final ClassWriter factoryWriter )
    {
        MethodVisitor create = factoryWriter.visitMethod( Opcodes.ACC_PUBLIC,
                                                          "fromPointerUntyped",
                                                          "(J)L" + ABSTRACT_STRUCTURE_TYPE + ";",
                                                          null,
                                                          null );
        create.visitCode();
        create.visitTypeInsn( NEW, this.implName );
        create.visitInsn( DUP );
        create.visitVarInsn( LLOAD, 1 );
        create.visitMethodInsn( INVOKESPECIAL,
                                this.implName,
                                "<init>",
                                "(J)V" );
        create.visitInsn( ARETURN );
        create.visitMaxs( 0, 0 );
        create.visitEnd();
    }

    /**
     * Interface for different types of structure builder.
     */
    interface StructureSubBuilder
    {
        /**
         * Creates the class.
         *
         * @param classWriter
         *            the class writer
         * @param methods
         *            the structure methods
         */
        void writeMainClassMethods( ClassWriter classWriter, List< Method > methods );

        /**
         * Writes the static constructor fragments.
         *
         * @param methodVisitor
         *            the method visitor
         */
        void writeStaticInitialiserFragment( MethodVisitor methodVisitor );

        /**
         * @return the largest field unit size, in bytes
         */
        int getLargestFieldUnitSize();

        /**
         * @return the structure's base type (byte-code) style
         */
        String getStructureBaseType();

        /**
         * @return name of the {@link StructureFactory} base type (byte-code style)
         */
        String getFactoryBaseType();

        /**
         * Writes the body of the {@link AbstractStructureFactory#allocateUntyped()}
         * method.
         *
         * @param methodVisitor
         *            the method visitor
         */
        void writeAllocateUntypedFactoryMethodImpl( MethodVisitor methodVisitor );
    }
}
